Skip to content

feat(stdlib): PixiJS 8.x Container transform-and-event surface (bindings #1)#502

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/pixi-container-accessors
May 31, 2026
Merged

feat(stdlib): PixiJS 8.x Container transform-and-event surface (bindings #1)#502
hyperpolymath merged 1 commit into
mainfrom
claude/pixi-container-accessors

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Summary

Extends the Tier-1 PixiJS binding (#446 row #1) with the 11 most-load-bearing accessors + on/off pointer-event registration. This is the largest single chunk of idaptik's src/bindings/Pixi.res surface still not bound — 215 src/app/*.res files depend on Container transforms and FederatedPointerEvent handlers, so this PR is a forward unblocker for the ReScript→AffineScript migration.

What lands

stdlib/Pixi.affine (+46 lines): 11 new extern fns.

Surface Externs
Container transforms pixiContainerSetScale, SetPivot, SetRotation, SetAlpha, SetZIndex, SetSortableChildren, SetEventMode, SetCursor
FederatedPointerEvent registration pixiContainerOn, pixiContainerOff
Sprite pixiSpriteSetAnchor

lib/codegen_deno.ml (+22 lines): 11 __as_* prelude helpers + 11 entries in the existing deno_builtins dispatch block, matching the existing wasmCall/motion/pixi pattern.

tests/codegen-deno/pixi_smoke.{affine,harness.mjs} (+80 lines combined): new smokeAccessorsFlow exercises every new extern via the existing harness pattern. MockContainer grows scale/pivot Point mocks + handler Map + the new fields; asserts handler identity is preserved across on(...)off(...).

docs/bindings-roadmap.adoc row #1 status note expanded — Container 8.x transform-and-event surface promoted from "deferred" to "landed"; remaining deferred items (typed FederatedPointerEvent accessors, parent read accessor with Option-null handling, Point/Rectangle/Circle helper types, sprite atlases, filters, hitArea) listed explicitly.

Design notes

Why pub extern fn pixiContainerSetEventMode(c, mode: String) rather than a sum type? Pixi 8's eventMode values are open strings ("static" / "dynamic" / "passive" / "none" / "auto"). A sum type would either freeze the set or require codegen-tagged-variant lowering that doesn't yet exist on the Deno-ESM backend (deferred to json.affine v0.3, mirroring the WasmValue decision in #467). Matches the existing pattern in stdlib/PixiUI.affine for slider.orientation.

Why handler: Json? The FederatedPointerEvent reaches the handler as a JS object; AffineScript-side code uses existing Json accessors to read e.global.x, e.target, etc. A typed FederatedPointerEvent extern type with dedicated accessor extern fns is the natural follow-up — captured in the roadmap row as a deferred item, not in this PR's scope. The Json handler avoids forcing every caller through a typed surface they may not want.

Anchor is on Sprite, not Container. Pixi 8 keeps the same split as 7.x — Container has no anchor. The binding mirrors that with pixiSpriteSetAnchor rather than putting it on pixiContainerSetAnchor.

Test plan

  • dune build bin/main.exe — clean (only the expected parser warnings)
  • dune runtest — 354 tests pass
  • tools/run_codegen_deno_tests.sh — all 17 harnesses including extended pixi_smoke.harness.mjs OK
  • CI build job
  • CI tools/run_codegen_deno_tests.sh job
  • CI governance + Hypatia (known baselines per repo CLAUDE.md may be red — those are not from this PR)

Refs

🤖 Generated with Claude Code

…ngs #1 expansion)

Extends the Tier-1 PixiJS binding with the 11 most-load-bearing
Container 8.x accessors + Sprite anchor + FederatedPointerEvent
on/off registration — the largest single chunk of idaptik's
src/bindings/Pixi.res surface still not bound.

stdlib/Pixi.affine (+46 lines):
  - Container: pixiContainerSetScale / SetPivot / SetRotation /
    SetAlpha / SetZIndex / SetSortableChildren / SetEventMode /
    SetCursor (8 transform/visual accessors)
  - Container: pixiContainerOn / pixiContainerOff (2 pointer-event
    registration externs taking the handler as opaque Json — the
    FederatedPointerEvent reaches the handler as Json which AS
    code reads via existing Json accessors; typed accessor surface
    deferred to follow-up)
  - Sprite: pixiSpriteSetAnchor (the anchor lives on Sprite, not
    Container — Pixi 8 keeps the same split)

lib/codegen_deno.ml: 11 __as_* prelude helpers + 11 dispatch table
entries, mirroring the existing wasmCall/motion/pixi pattern.

tests/codegen-deno/pixi_smoke.{affine,harness.mjs}: new
smokeAccessorsFlow exercises every new extern; harness's
MockContainer grows scale/pivot Point mocks + handler Map +
sortableChildren/eventMode/cursor/zIndex/alpha/rotation fields;
asserts handler identity preserved across on() + off().

docs/bindings-roadmap.adoc row #1 status note updated to reflect
the new surface + remaining deferred items (typed
FederatedPointerEvent accessors, parent read accessor with
Option-null handling, Point/Rectangle/Circle helper types, sprite
atlases, filters, hitArea).

Refs #446 — Tier 1 #1 expansion.
@hyperpolymath hyperpolymath force-pushed the claude/pixi-container-accessors branch from 15f68ce to a464aa2 Compare May 31, 2026 07:33
@hyperpolymath hyperpolymath merged commit 44a0685 into main May 31, 2026
15 of 24 checks passed
@hyperpolymath hyperpolymath deleted the claude/pixi-container-accessors branch May 31, 2026 07:33
hyperpolymath added a commit that referenced this pull request May 31, 2026
) (#506)

## Summary

Ships the first concrete surface for Tier-1 #9 of #446 — **IPC /
structuredClone** for host↔guest message passing. This is the binding
`idaptik-ums` Gossamer IPC for level I/O depends on, and the pattern any
embedded-engine binding in the estate needs.

## What lands

`stdlib/Ipc.affine` (+105 lines, new module): 2 extern types + 9 extern
fns covering MessageChannel construction, port handoff, post +
onmessage, start/close lifecycle, generic `targetPostMessage`, and
`structuredCloneValue`.

| Surface | Externs |
|---|---|
| Channel | `MessageChannel` / `MessagePort` opaque types;
`messageChannelNew` / `messageChannelPort1` / `messageChannelPort2` |
| Port | `messagePortPostMessage` / `messagePortOnMessage` /
`messagePortStart` / `messagePortClose` |
| Cross-context | `targetPostMessage` (Worker / iframe.contentWindow /
self-from-worker) |
| Deep-clone | `structuredCloneValue` |

`lib/codegen_deno.ml` (+23 lines): 9 `__as_*` prelude helpers + 9
dispatch entries adjacent to the pixiSound block. No consumer init —
`MessageChannel` / `MessagePort` / `structuredClone` are standard
web-platform globals.

`tests/codegen-deno/ipc_smoke.{affine,harness.mjs}` (+98 lines
combined): port-pair postMessage round-trip with handler-identity
preservation, standalone close lifecycle, target-post stub, and
structuredClone deep-copy with reference-distinctness assertions across
nested arrays + objects.

`docs/bindings-roadmap.adoc` row #9 promoted `○ → ◑`; deferred items
listed.

## Design notes

**Why is `MessagePort` an opaque `extern type` instead of a
record/tagged-union?** Same reason `WasmExports` and `WasmValue` (#467)
are opaque — the Deno-ESM backend doesn't yet have tagged-variant
codegen (deferred to json.affine v0.3), and a real `MessagePort` carries
internal worker-thread state that isn't usefully observable from
AffineScript anyway. The handler observes the `MessageEvent` as opaque
`Json` and reads `event.data` via the existing Json accessors. A typed
`MessageEvent` extern-type with dedicated accessor externs is the
natural follow-up axis, captured in the roadmap deferred-items list.

**Why is the host responsible for closing the ports?** Inline `close()`
calls in `smokeChannelFlow` would race the microtask-async delivery —
MessagePort drops queued messages on close. The fixture surfaces this as
a documented authoring pattern (it lives in a top-of-file comment) so
anyone writing IPC code with this binding doesn't get bitten.

**Why `setTimeout(50)` in the harness instead of
`setImmediate`/microtask flush?** Empirically verified — Node 20's
`worker_threads`-backed `MessageChannel` batches delivery beyond a
single setImmediate tick. The comment in the harness records the test
that surfaced it (a standalone `node` repl reproducer with the same
shape).

**Why no `transfer` list yet?** Owner directive at #455 (Option-B
kickoff scope) — ship the generic surface, layer typed-and-richer
variants as follow-ups once usage patterns crystallise. Same pattern as
`WasmValue` — opaque tagged scalar first, typed wrappers next.

## Test plan

- [x] `dune build bin/main.exe` — clean (only the expected parser
warnings)
- [x] `dune runtest` — 354 tests pass
- [x] `tools/run_codegen_deno_tests.sh` — all 18 harnesses including the
new `ipc_smoke.harness.mjs` OK
- [ ] CI build job
- [ ] CI `tools/run_codegen_deno_tests.sh` job
- [ ] CI governance + Hypatia (known baselines per repo CLAUDE.md may be
red — not from this PR)

## Refs

- Umbrella: #446 (Tier 1 — idaptik blockers)
- Tier-1 sub-issue: #450
- Row updated: `docs/bindings-roadmap.adoc` row #9
- Pattern siblings: #467 (`wasm_export_call` Option-B), #474 (Zig-FFI
patterns doc), #502 (PixiJS 8.x Container)
- Adjacent: Tier-3 #25 Web Workers — `Worker` constructors would consume
this surface

🤖 Generated with [Claude Code](https://claude.com/claude-code)
hyperpolymath added a commit that referenced this pull request May 31, 2026
…#510)

## Summary

Ships the Canvas 2D half of Tier-1 #8 of #446. This is the surface
`idaptik-ums` (App.res: 1178 LoC of DOM + canvas) and every non-Pixi UI
in the estate has been waiting for; WebGL / WebGL2 / WebGPU stays at `○`
until a consumer surfaces a concrete need.

## What lands

`stdlib/Canvas.affine` (+170 lines, new module): 1 extern type (`Ctx2D`)
+ 26 extern fns covering the full idaptik-ums-relevant Canvas 2D
surface:

| Surface | Externs |
|---|---|
| Context acquisition | `canvasGetContext2D` |
| Styles | `canvasFillStyle` / `StrokeStyle` / `LineWidth` /
`GlobalAlpha` |
| Rectangles | `canvasFillRect` / `StrokeRect` / `ClearRect` |
| Paths | `canvasBeginPath` / `ClosePath` / `MoveTo` / `LineTo` / `Arc`
/ `Fill` / `Stroke` |
| Transform stack | `canvasSave` / `Restore` / `Translate` / `Rotate` /
`Scale` |
| Text | `canvasFont` / `TextAlign` / `TextBaseline` / `FillText` /
`StrokeText` / `MeasureText` |
| Images | `canvasDrawImage` / `canvasDrawImageScaled` |

`lib/codegen_deno.ml` (+58 lines): 26 `__as_canvas*` prelude helpers +
26 dispatch entries adjacent to the Ipc block.

`tests/codegen-deno/canvas_smoke.{affine,harness.mjs}` (+160 lines
combined): 5 distinct smoke functions exercise every shipped extern. The
`MockCtx2D` records every method call as a typed-op tuple so the test
asserts call order + arguments, not just side-effects. The
`canvasMeasureText` round-trip exercises the `Json`-return shape (the
`TextMetrics`-shaped object).

`docs/bindings-roadmap.adoc` row #8 status promoted `○ → ◑`; deferred
items captured (WebGL, gradients/patterns, ImageData, curve primitives,
compositing/clip).

## Design notes

**Why is `HTMLCanvasElement` not its own extern type?** The
canvas-creation entry point is host-dependent — browser
`document.createElement("canvas")` vs idaptik's pre-existing DOM tree vs
jsdom-under-Deno. Treating the canvas as opaque `Json` defers the typed
wrapper to the natural follow-up once `affinescript-dom` lands runtime
support (currently blocked on the wasm-codegen `for-in` / `while` gap,
issue #255).

**Why fix `arc` counter-clockwise to `false`?** The 6-arg shape (`ccw`
boolean) would force every call site to pass a literal `false`. The
5-arg shape covers the overwhelming majority of consumers; a typed
wrapper with the `ccw` flag is a follow-up if a consumer surfaces the
need.

**Why two separate `drawImage` externs instead of one with optional
args?** AffineScript doesn't have JS-style variadic functions; making
`w` and `h` `Option<Float>` would force every natural-size call to
thread `None`s. The two-extern split keeps each call site simple.

**Why open-string `eventMode` / `textAlign` / `textBaseline`?** Same
rationale as the PixiJS expansion (#502) — Pixi 8's `eventMode` and
Canvas's text-alignment values are open enumerations, and a sum-type
binding would either freeze the set or require tagged-variant codegen
that doesn't exist on the Deno-ESM backend yet (deferred to json.affine
v0.3). Consistency with existing patterns.

## Test plan

- [x] `dune build bin/main.exe` — clean (only the expected parser
warnings)
- [x] `dune runtest --force` — 356 tests pass (was 354 pre-PR; +2 from
new Canvas + Ipc modules' AOT smoke)
- [x] `tools/run_codegen_deno_tests.sh` — all 19 harnesses including the
new `canvas_smoke.harness.mjs` OK
- [ ] CI build job
- [ ] CI `tools/run_codegen_deno_tests.sh` job
- [ ] CI governance + Hypatia (6 baselines per repo CLAUDE.md may be red
— pre-existing on main, not regressions)

## Refs

- Umbrella: #446 (Tier 1 — idaptik blockers)
- Tier-1 sub-issue: #450
- Row updated: `docs/bindings-roadmap.adoc` row #8
- Pattern siblings shipped this session: #502 (PixiJS Container
expansion), #506 (Ipc.affine)
- Adjacent / future: `affinescript-dom` (gated on #255), WebGL/WebGPU
(Tier 3 axis, not surfaced as need yet)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@github-actions
Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 83 issues detected

Severity Count
🔴 Critical 2
🟠 High 13
🟡 Medium 68

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action ons/checkout@v6\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action land/setup-deno@v2\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in affine-vscode-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "affine-vscode-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant